package orbito;

import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.geom.AffineTransform;
import java.util.Collection;
import java.util.List;
import java.util.Random;

import javax.swing.SwingUtilities;

import orbito.graphics.ViewFocus;
import orbito.graphics.MessageQueue;
import orbito.graphics.Viewport;
import orbito.scene.SceneGraph;
import orbito.scene.SceneStore;
import orbito.simulation.Simulator;
import orbito.simulation.UnthreadedSimulator;
import orbito.ui.InputStore;
import orbito.ui.OrbitoFrame;
import orbito.util.GfxUtils;
import orbito.util.Stopwatch;

public class DefaultGameController implements GameController {
	
	// The game frame.
	OrbitoFrame frame;
	
	// The time step simulator.
	Simulator sim;
	
	// The scene store and initial main game scene.
	SceneStore sceneStore;
	SceneGraph mainScene;
	
	// The viewport.
	Viewport viewport;
	
	// Stopwatch to measure time in between renders.
	Stopwatch renderSw;
	
	// The average render dt.
	double displayRenderDt = 0;
	
	// On-screen messages.
	MessageQueue messages;
	
	public DefaultGameController(OrbitoFrame frame) {
		// Set the frame.
		this.frame = frame;
		
		// Create viewport.
		viewport = new Viewport();
		
		// Create render time stopwatch.
		renderSw = new Stopwatch();
	}
	
	// -------------------------------------------
	// GameController Interface.
	
	public void seed(long seed) {
		GameSettings.seed = seed;
	}
	
	public void init() {
		// Create simulator only once.
		if (sim == null) {
			// 1. Create the simulator.
			sim = new UnthreadedSimulator(GameSettings.initDt);
			sim.setGameController(this);
			sim.setDtDownLock(GameSettings.dtDownLock);
			sim.setDtUpLock(GameSettings.dtUpLock);
		} else {
			// 2. Invoke reset on the simulator.
			sim.reset();
		}
		
		// Create the message queue.
		messages = new MessageQueue(GameSettings.messageRenderCount);
		messages.addMessage("WASD to move, XZ to zoom, PGUP/PGDN to rotate, TR for time compression, F11 to reset, F12 to reseed", 30);
		messages.addMessage("F1 to toggle view info, F2 to toggle sim info, F3 to toggle overlays, F4 to toggle trails", 30);
		
		// Init the scene store.
		sceneStore = new SceneStore();
		
		// Set the main scene and the main view.
		mainScene = sceneStore.getScene("default");
		
		// Init the main scene.
		mainScene.init(new Random(GameSettings.seed), messages);
		
		// Update the UI.
		invokeUpdateMenuUi();
		invokeUpdateViewFocusUi();
		invokeUpdateScenesUi();
	}
	
	public void start() {
		if (mainScene != null) {
			// Reset the render stopwatch.
			renderSw.reset();
			
			// Start the simulator.
			sim.start();
		}
	}
	
	public void pause() {
		if (mainScene != null) {
			sim.pause();
		}
	}
	
	public void unpause() {
		if (mainScene != null) {
			sim.start();
		}
	}
	
	public void step(double t, double dt) {
		if (mainScene != null) {
			synchronized (mainScene) {
				mainScene.step(t, dt);
			}
		}
	}
	
	public void render(Graphics2D g) {
		AffineTransform tr = g.getTransform();
		double t = sim.getTime(), dt = renderSw.getElapsedTime();
		
		// Smooth the display dt.
		displayRenderDt = displayRenderDt * 0.95 + dt * 0.05;
		
		// Setup rendering.
		setupRendering(g);
		
		// 1. If there is no selected scene...
		if (mainScene == null) {
			// Clear screen to gray.
			clearScreen(g, Color.GRAY);
			
			// Render message.
			g.setColor(Color.BLACK);
			GfxUtils.drawText(g, "No scene loaded", (int) viewport.getWidth() / 2, (int) viewport.getHeight() / 2, GfxUtils.TEXT_CENTER);
			
			// Make sure sim is reset.
			sim.reset();
		}
		
		// 2. Otherwise, step the simulator, render the scene, and render info.
		else {
			// Clear screen to black.
			// TODO Get a clear color from the scene.
			clearScreen(g, Color.BLACK);
			
			// Step the simulator, if needed.
			sim.step();
			
			// Synchronize access to the scene.
			synchronized (mainScene) {
				// Manage input here.
				handleInput(dt);
				mainScene.handleInput(dt);
				
				// Calculate view transform.
				AffineTransform tx = viewport.getTransform();
				tx.concatenate(mainScene.getCurrentView().getTransform());
				
				// Set view transform and render scene graph.
				g.transform(tx);
				mainScene.render(g, t, dt);
				
				// Reset transform.
				g.setTransform(tr);
				
				// Render scene graph info, if any.
				mainScene.renderInfo(g, viewport, tx, t, dt);
				
				// Render info, if any.
				renderInfo(g, viewport, tx, t, dt);
			}
		}
		
		g.setTransform(tr);
	}
	
	public void handleInput(double dt) {
		if (mainScene != null) {
			// Handle camera translation.
			if (InputStore.isKeyPressed(KeyEvent.VK_A) || InputStore.isKeyPressed(KeyEvent.VK_NUMPAD4)) {
				mainScene.getCurrentView().translateCamera(-500 * dt, 0);
			}
			if (InputStore.isKeyPressed(KeyEvent.VK_D) || InputStore.isKeyPressed(KeyEvent.VK_NUMPAD6)) {
				mainScene.getCurrentView().translateCamera(500 * dt, 0);
			}
			if (InputStore.isKeyPressed(KeyEvent.VK_W) || InputStore.isKeyPressed(KeyEvent.VK_NUMPAD8)) {
				mainScene.getCurrentView().translateCamera(0, -500 * dt);
			}
			if (InputStore.isKeyPressed(KeyEvent.VK_S) || InputStore.isKeyPressed(KeyEvent.VK_NUMPAD2)) {
				mainScene.getCurrentView().translateCamera(0, 500 * dt);
			}
			if (InputStore.isKeyPressed(KeyEvent.VK_Q) || InputStore.isKeyPressed(KeyEvent.VK_NUMPAD5)) {
				mainScene.getCurrentView().resetFocus();
			}
			
			// Handle camera rotation.
			if (InputStore.isKeyPressed(KeyEvent.VK_PAGE_UP) || InputStore.isKeyPressed(KeyEvent.VK_NUMPAD9)) {
				mainScene.getCurrentView().rotateCamera(3.0 * dt);
			}
			if (InputStore.isKeyPressed(KeyEvent.VK_PAGE_DOWN) || InputStore.isKeyPressed(KeyEvent.VK_NUMPAD7)) {
				mainScene.getCurrentView().rotateCamera(-3.0 * dt);
			}
			
			// Handle camera zoom.
			if (InputStore.isKeyPressed(KeyEvent.VK_Z) || InputStore.isKeyPressed(KeyEvent.VK_DIVIDE)) {
				mainScene.getCurrentView().zoomCamera(-2.0 * dt);
			}
			if (InputStore.isKeyPressed(KeyEvent.VK_X) || InputStore.isKeyPressed(KeyEvent.VK_MULTIPLY)) {
				mainScene.getCurrentView().zoomCamera(2.0 * dt);
			}
		}
	}
	
	public void updateViewport(int width, int height) {
		viewport.setSize(width, height);
	}
	
	// -------------------------------------------
	// Scene Selection.

	public Collection<SceneGraph> getScenes() {
		return sceneStore.getAllScenes();
	}

	public SceneGraph getCurrentScene() {
		return mainScene;
	}

	public void loadScene(SceneGraph scene) {
		if (scene == null) {
			clearCurrentScene();
		} else {
			// Reset the simulator, init the new scene, and set the new main scene.
			sim.reset();
			scene.init(new Random(GameSettings.seed), messages);
			mainScene = scene;
			
			// Update the UI.
			invokeUpdateMenuUi();
			invokeUpdateViewFocusUi();
		}
	}
	
	public void resetCurrentScene() {
		if (mainScene != null) {
			// Reset the simulator.
			sim.reset();
			
			// Init the main scene.
			mainScene.init(new Random(GameSettings.seed), messages);
			
			// Update the UI.
			invokeUpdateMenuUi();
			invokeUpdateViewFocusUi();
		}
	}
	
	public void clearCurrentScene() {
		// Reset the simulator, and clear the main scene.
		sim.reset();
		mainScene = null;
		
		// Update the UI.
		invokeUpdateMenuUi();
		invokeUpdateViewFocusUi();
	}
	
	// -------------------------------------------
	// View Focus Selection.
	
	public List<ViewFocus> getViewFocusItems() {
		if (mainScene == null) {
			return null;
		} else {
			return mainScene.getViewFocusItems();
		}
	}
	
	public ViewFocus getCurrentViewFocus() {
		if (mainScene == null) {
			return null;
		} else {
			return mainScene.getCurrentView().getFocus();
		}
	}
	
	public void setCurrentViewFocus(ViewFocus focus) {
		if (mainScene != null) {
			mainScene.getCurrentView().setFocus(focus);
		}
	}
	
	public void clearCurrentViewFocus() {
		if (mainScene != null) {
			mainScene.getCurrentView().clearFocus();
		}
	}
	
	// -------------------------------------------
	// Helper Methods.
	
	private void clearScreen(Graphics2D g, Color clearColor) {
		g.setColor(clearColor);
		g.fillRect(0, 0, (int) viewport.getWidth(), (int) viewport.getHeight());
	}
	
	private void setupRendering(Graphics2D g) {
		// Setup anti-alias.
		if (GameSettings.performAntiAlias) {
			g.setRenderingHint(
					RenderingHints.KEY_ANTIALIASING,
					RenderingHints.VALUE_ANTIALIAS_ON);
		} else {
			g.setRenderingHint(
					RenderingHints.KEY_ANTIALIASING,
					RenderingHints.VALUE_ANTIALIAS_OFF);
		}
		
		// Setup text anti-alias.
		if (GameSettings.performTextAntiAlias) {
			g.setRenderingHint(
					RenderingHints.KEY_TEXT_ANTIALIASING,
					RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
		} else {
			g.setRenderingHint(
					RenderingHints.KEY_TEXT_ANTIALIASING,
					RenderingHints.VALUE_TEXT_ANTIALIAS_OFF);
		}
	}
	
	private void renderInfo(Graphics2D g, Viewport v, AffineTransform tx, double t, double dt) {
		// Paint simulator info.
		if (GameSettings.renderSimInfo) {
			sim.renderInfo(g, v, tx, t, dt);
		}
		
		// Paint camera cross.
		if (GameSettings.renderCameraInfo) {
			mainScene.getCurrentView().renderCameraCross(g, v, tx, t, dt);
		}
		
		// Paint the on-screen messages.
		messages.processMessages(t, dt);
		if (GameSettings.renderMessages) {
			messages.renderMessages(g, v, tx, t, dt);
		}
		
		// Paint paused message.
		if (isPaused()) {
			g.setColor(Color.RED);
			String str = "press PAUSE to unpause";
			GfxUtils.drawText(g, str, (int) v.getWidth() / 2, (int) v.getHeight() / 3, GfxUtils.TEXT_CENTER);
		}
		
		// Paint framerate.
		if (GameSettings.renderAnimationInfo) {
			g.setColor(Color.RED);
			String str = String.format("FPS: %.1f", 1 / displayRenderDt);
			g.drawString(str, (int) v.getWidth() - 60, 15);
		}
	}
	
	// -------------------------------------------
	// UI Interface.
	
	public boolean isPaused() {
		return sim.isPaused();
	}
	
	public boolean isDtDownLock() {
		return sim.isDtDownLock();
	}
	
	public void setDtDownLock(boolean b) {
		sim.setDtDownLock(b);
	}
	
	public boolean isDtUpLock() {
		return sim.isDtUpLock();
	}
	
	public void setDtUpLock(boolean b) {
		sim.setDtUpLock(b);
	}

	public void decreaseTimeCompression() {
		sim.decreaseTimeCompression();
	}

	public void increaseTimeCompression() {
		sim.increaseTimeCompression();
	}
	
	public void invokeUpdateMenuUi() {
		SwingUtilities.invokeLater(new Runnable() {
			public void run() {
				frame.updateMenuUi();
			}
		});
	}
	
	public void invokeUpdateScenesUi() {
		SwingUtilities.invokeLater(new Runnable() {
			public void run() {
				frame.updateScenesUi();
			}
		});
	}
	
	public void invokeUpdateViewFocusUi() {
		SwingUtilities.invokeLater(new Runnable() {
			public void run() {
				frame.updateViewFocusUi();
			}
		});
	}
	
	// -------------------------------------------
	// Event Listeners.
	
	public void keyPressed(KeyEvent arg0) { }
	public void keyReleased(KeyEvent arg0) { }
	public void keyTyped(KeyEvent arg0) { }
	public void mouseClicked(MouseEvent arg0) { }
	public void mouseEntered(MouseEvent arg0) { }
	public void mouseExited(MouseEvent arg0) { }
	public void mousePressed(MouseEvent arg0) { }
	public void mouseReleased(MouseEvent arg0) { }
}
